﻿/**

 * Copyright (c) 2015-2016, FastDev 刘强 (fastdev@163.com).

 *

 * Licensed under the Apache License, Version 2.0 (the "License");

 * you may not use this file except in compliance with the License.

 * You may obtain a copy of the License at

 *

 *      http://www.apache.org/licenses/LICENSE-2.0

 *

 * Unless required by applicable law or agreed to in writing, software

 * distributed under the License is distributed on an "AS IS" BASIS,

 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.

 * See the License for the specific language governing permissions and

 * limitations under the License.

 */

using System;
using System.Diagnostics.CodeAnalysis;
using RazorEngine.Compilation.CSharp;
using RazorEngine.Compilation.VisualBasic;
using System.Security;
using System.IO;
using System.Reflection;
using System.Diagnostics.Contracts;
using System.CodeDom.Compiler;
using System.Security.Principal;
using RazorEngine.Templating;
using System.Security.Permissions;
using System.Linq;
using System.Collections.Generic;
using RazorEngine.Compilation.ReferenceResolver;
using RazorEngine.Compilation;

namespace OF.SmartMVC.Core.Compile
{
    public class StoreCSharpDirectCompilerService : CSharpDirectCompilerService
    {
        private static Action<TypeContext, string> setClassNameAction = null;
        private List<Assembly> referAssemblis = null;
        private string template;
        static StoreCSharpDirectCompilerService()
        {
            MethodInfo setClassNameMethod = typeof(TypeContext).GetMethod("set_ClassName", BindingFlags.Instance | BindingFlags.CreateInstance | BindingFlags.NonPublic | BindingFlags.Public);
            setClassNameAction = (Action<TypeContext, string>)Delegate.CreateDelegate(typeof(Action<TypeContext, string>), setClassNameMethod);
        }

        public StoreCSharpDirectCompilerService(List<Assembly> referAssemblis, string template)
        {
            this.referAssemblis = referAssemblis;
            this.template = template;
        }

        /// <summary>
        /// Returns a set of references that must be referenced by the compiled template.
        /// </summary>
        /// <returns>The set of references.</returns>
        public override IEnumerable<CompilerReference> IncludeReferences()
        {
            //RazorEngine.Compilation.ReferenceResolver.CompilerReference.FileReference
            var result = referAssemblis.Select(RazorEngine.Compilation.ReferenceResolver.CompilerReference.From);
            return result;
        }

        protected override string GetTemporaryDirectory()
        {
            return GetGenDllDirectory();
        }
        


        public static string GetTempDirectory()
        {
            string filePath = EngineHelper.Config.GenerateDllPath;
            filePath = Path.Combine(filePath, "Temp");
            if (!Directory.Exists(filePath))
            {
                Directory.CreateDirectory(filePath);
            }
            return filePath;
        }

        private static string GetGenDllDirectory()
        {
            string filePath = EngineHelper.Config.GenerateDllPath;
            if (!Directory.Exists(filePath))
            {
                Directory.CreateDirectory(filePath);
            }
            return filePath;
        }

        #region Methods

        /// <summary>
        /// Creates the compile results for the specified <see cref="TypeContext"/>.
        /// </summary>
        /// <param name="context">The type context.</param>
        /// <returns>The compiler results.</returns>
        [Pure]
        [SecurityCritical]
        private static CompilerResults Compile(string assemblyName)
        {
            var fileAssemblies = assemblies.ToArray();
            var haveMscorlib = fileAssemblies.Any(a => a.Contains("mscorlib.dll"));
            string tempFilePath = GetTempDirectory();
            var @params = new CompilerParameters
            {
                GenerateInMemory = false,
                GenerateExecutable = false,
                IncludeDebugInformation = EngineHelper.Config.IncludeDebugInformation,
                TreatWarningsAsErrors = false,
                TempFiles = new TempFileCollection(tempFilePath, EngineHelper.Config.IsKeepCompileFiles),
                CompilerOptions =
                    string.Format("/target:library /optimize /define:RAZORENGINE {0}",
                        haveMscorlib ? "/nostdlib" : "")
            };

            @params.ReferencedAssemblies.AddRange(fileAssemblies);
            var assemblyFullName = Path.Combine(GetGenDllDirectory(),
                String.Format("{0}.dll", assemblyName));
            @params.TempFiles.AddFile(assemblyFullName, true);
            @params.OutputAssembly = assemblyFullName;
            var results = Provider.CompileAssemblyFromSource(@params, sourceCodeList.ToArray());
            return results;    
        }

        private static List<TypeContext> typeContextList = new List<TypeContext>();
        private static HashSet<string> assemblies = new HashSet<string>();
        private static List<string> sourceCodeList = new List<string>();
        private static CodeDomProvider Provider = null;
        public static void CompileAll(string assemblyName)
        {
            var compileResult = Compile(assemblyName);

            if (compileResult.Errors != null && compileResult.Errors.HasErrors)
            {
                throw new Exception("compileResult.Errors:" + compileResult.Errors[0].ToString());
            }
            // Make sure we load the assembly from a file and not with
            // Load(byte[]) (or it will be fully trusted!)
            var assemblyPath = compileResult.PathToAssembly;
            compileResult.CompiledAssembly = Assembly.LoadFile(assemblyPath);
            foreach (var context in typeContextList)
            {
                var type = compileResult.CompiledAssembly.GetType(DynamicTemplateNamespace + "." + context.ClassName);
                if (type == null)
                {
                    try
                    {
                        compileResult.CompiledAssembly.GetTypes();
                    }
                    catch (Exception e)
                    {
                        throw new Exception("Unable to load types of the laded assembly" + e);
                    }
                    // if we are here we just throw
                    throw new Exception("We could not find the type in the compiled assembly!");
                }
            }
        }

        [Pure, SecurityCritical]
        private Tuple<Type, CompilationData> CompileTypeImpl(TypeContext context)
        {
            var tempAssemblies = GetAllReferences(context);
            var tempFullNames = tempAssemblies
                .Select(a => a.GetFile(
                    msg => new ArgumentException(
                        string.Format(
                            "Unsupported CompilerReference given to CodeDom compiler (only references which can be resolved to files are supported: {0})!",
                            msg))))
                .Where(a => !string.IsNullOrWhiteSpace(a))
                .Distinct(StringComparer.InvariantCultureIgnoreCase)
                .ToArray();
            foreach (var fullName in tempFullNames)
            {
                if (!assemblies.Contains(fullName))
                {
                    assemblies.Add(fullName);
                }
            }

            typeContextList.Add(context);
            sourceCodeList.Add(GetCodeCompileUnit(context));
            Provider = CodeDomProvider;
            string testPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test_Gen_Temp");
            return new Tuple<Type, CompilationData>(this.GetType(), new CompilationData(string.Empty, testPath));
        }


        [Pure, SecurityCritical]
        private Tuple<Type, CompilationData> CompileType_Windows(TypeContext context)
        {
            // NOTE: The static constructor of WindowsImpersonationContext can fail, 
            // that's why we need to do that in a seperate method
            // -> Static constructor will not run

            WindowsImpersonationContext wic = WindowsIdentity.Impersonate(IntPtr.Zero);
            try
            {
                return CompileTypeImpl(context);
            }
            finally
            {
                wic.Undo();
            }
        }

        /// <summary>
        /// Compiles the type defined in the specified type context.
        /// </summary>
        /// <param name="context">The type context which defines the type to compile.</param>
        /// <returns>The compiled type.</returns>
        [Pure, SecurityCritical]
        public override Tuple<Type, CompilationData> CompileType(TypeContext context)
        {
            setClassNameAction(context, template.Replace('.', '_'));

            if (context == null)
                throw new ArgumentNullException("context");

            (new PermissionSet(PermissionState.Unrestricted)).Assert();
            var isMono = Type.GetType("Mono.Runtime") != null;
            if (!isMono)
            {
                return CompileType_Windows(context);
            }
            else
            {
                return CompileTypeImpl(context);
            }
        }
        #endregion
    }
}


/*
    if (results.Errors != null && results.Errors.HasErrors)
    {
               
        string path = Path.Combine(GetGenDllDirectory(), "error.txt");
        string fileAssemblyNames = string.Join("\r\n", fileAssemblies);
        string content = @params.CompilerOptions + "\r\nReferenced:" + fileAssemblyNames + "\r\n" + string.Join("\r\n\r\n\r\n", sourceCodeList.ToArray());
        byte[] btArray = System.Text.Encoding.UTF8.GetBytes(content);
        using (FileStream fs = new FileStream(path, FileMode.Append))
        {
            fs.Write(btArray, 0, btArray.Length);
            fs.Flush(true);
        }
        return results;
    }
    else
    {
        //Directory.Delete(tempFilePath, true);
        return results;
    }
    */